package com.putable.nmc.world;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.InputMismatchException;
import java.util.Map;
import java.util.Scanner;

/**
 * A model of the world outside the DMV. Handles generating information about
 * arriving customers, collects statistics about customer satisfaction and DMV
 * costs and revenues, and judges whether the DMV has satisfied its business
 * requirements.
 * 
 * <p>
 * The primary interaction between a World and a DMV is by exchanging 'CIT'
 * objects -- 'customer information tokens'. Note that CIT's are meant to be
 * descriptions or representations of customers, but are <i>not</i> actual
 * customers themselves. The reason for the existence of CIT's is so that the
 * World can communicate with a DMV without having to know anything about the
 * DMV's internal class design.
 * 
 * <p>
 * See class Demo for a minimal example of code that interfaces with a World
 * object.
 * 
 * @author ackley
 * @version 1.0
 * @see CIT
 * @see com.putable.nmc.dmv.demo.Demo
 * 
 */
public class World {
    private String name;
    private double lastArrivalTime = 60 * 60 * 10;
    private int theDMVCapacity;
    private int nManagers = -1;
    private int nGoodTellers;
    private int nBadTellers;
    private double sManager;
    private double sGoodTeller;
    private double sBadTeller;
    private CustomerDistribution[] custDists;
    private CustomerDistribution currentDistribution = null;
    private XRandom random = new XRandom(1);

    private Map<CustomerType, CIT> nextCITs = new EnumMap<CustomerType, CIT>(
            CustomerType.class);
    private int customersGenerated = 0;

    private class Statistics {
        private int nEntered;
        private int nExited;
        private int nSatisfied;
        private int nGaveUp;
        private int nTurnedAway;
        private int nComplained;
        private int earnings;

        public class CustomerReport {
            private int totalCustomers = nSatisfied + nGaveUp + nTurnedAway;
            private double CSI = 100.0 * (nSatisfied - nComplained)
                    / Math.max(1, totalCustomers);
            private boolean acceptable = CSI >= 80.0;

            public boolean isOK() {
                return acceptable;
            }

            public String toString() {
                return String.format("Customer Satisfaction Report\n"
                        + "-------- ------------ ------\n"
                        + "  Total customers: %,d\n" + "  %,10d satisfied\n"
                        + "  %,10d gave up\n" + "  %,10d turned away\n"
                        + "  %,10d complained\n" + "  ---------------\n"
                        + " CUSTOMER SATISFACTION INDEX: %6.2f\n"
                        + "   Acceptable results: %s\n", totalCustomers,
                        nSatisfied, nGaveUp, nTurnedAway, nComplained, CSI,
                        acceptable);
            }
        }

        public class BudgetReport {
            private double costManager = nManagers * sManager / 200.0;
            private double costGoodTeller = nGoodTellers * sGoodTeller / 200.0;
            private double costBadTeller = nBadTellers * sBadTeller / 200.0;
            private double totalStaffCost = costManager + costGoodTeller
                    + costBadTeller;
            private double costDirectorsCut = 500.0;
            private double costDMVLobbyists = 325.0;
            private double costMaintenance = 5.0 * theDMVCapacity;
            private double costNonStaff = earnings * 0.75 + costMaintenance
                    + costDMVLobbyists + costDirectorsCut;
            private double profitForStaff = earnings - costNonStaff;
            private double staffBalance = profitForStaff - totalStaffCost;
            private boolean inTheBlack = staffBalance > 0.0;

            public boolean isOK() {
                return inTheBlack;
            }

            public String toString() {
                return String.format("DMV Budget Report\n"
                        + "--- ------ ------\n"
                        + "   Managers: %3d, per day total $%,10.2f\n"
                        + "   Tellers2: %3d, per day total $%,10.2f\n"
                        + "   Tellers1: %3d, per day total $%,10.2f\n" + "\n"
                        + "  $%,12.2f Gross income/day\n"
                        + "  $%,12.2f Non-salary fixed costs/day\n"
                        + "  $%,12.2f Available for salary/day\n"
                        + "  $%,12.2f Staff cost/day\n" + "  ---------------\n"
                        + "  $%(,12.2f NET PROFIT/DAY\n"
                        + "   Acceptable results: %s\n", nManagers,
                        costManager, nBadTellers, costBadTeller, nGoodTellers,
                        costGoodTeller, 1.0 * earnings, costNonStaff,
                        profitForStaff, totalStaffCost, staffBalance,
                        inTheBlack);
            }
        }

        public class EmploymentReport {
            CustomerReport cr;
            BudgetReport br;

            public boolean isPromoted() {
                boolean c = cr.isOK();
                boolean b = br.isOK();
                return c && b;
            }

            public EmploymentReport(CustomerReport cr, BudgetReport br) {
                this.cr = cr;
                this.br = br;
            }

            public String toString() {
                boolean c = cr.isOK();
                boolean b = br.isOK();
                if (c && b)
                    return "You satisfied the customers"
                            + " and turned a profit!\n"
                            + "Congratulations, Mr. VICE PRESIDENT!\n";
                if (c && !b)
                    return "Well, you satisfied the customers"
                            + " but you BUSTED THE BUDGET!\n"
                            + "You're FIRED!\n";
                if (!c && b)
                    return "Well, you made money and that's always good,"
                            + " but the CUSTOMERS HATE YOU!\n"
                            + "You're FIRED!\n";
                if (!c && !b)
                    return "Well, you busted the budget, AND alienated"
                            + " the customers.\nWho do you think you are, TZ?"
                            + " \nYou're FIRED!\n";
                throw new IllegalStateException(
                        "Unreachable, you dimwit compiler");
            }
        }

        public void logIn(CIT cit) {
            ++nEntered;
        }

        public int getCITsOut() {
            return nEntered - nExited;
        }

        public void logOut(CIT cit) {
            ++nExited;
            int diff = getCITsOut();
            if (diff < 0)
                throw new IllegalStateException("Too many exits");
            if (!cit.getEntered())
                ++nTurnedAway;
            else if (cit.getSatisfied()) {
                ++nSatisfied;
                earnings += cit.getTask().getCost();
            } else {
                ++nGaveUp;
                if (cit.getComplained())
                    ++nComplained;
            }
        }
    }

    private Statistics stats = new Statistics();

    /**
     * Tells the world that 'tok' has 'left the building'. Must be called
     * precisely once on each CIT object returned by World.getNextCIT().
     * 
     * <p>
     * Once the CIT is returned to the World, it is examined in an 'exit
     * interview' to determine how things went. CIT's marked as 'completed'
     * improve the earnings of the DMV and increase the chances of success,
     * while CIT's not so marked may lead to customer complaints and increase
     * the chances of failure.
     * 
     * @param tok
     *            The CIT being returned to the World, that had previously been
     *            given to a DMV by getNextCIT.
     * @throws NullPointerException
     *             if tok is null
     * @throws IllegalStateException
     *             if tok wasn't in the building anyway
     */
    public void exitInterview(CIT tok) {
        if (tok == null)
            throw new NullPointerException("CIT must not be null");

        double done = tok.getTaskCompletedAt();
        if (done >= 0) {
            tok.setSatisfied(true);
        } else {
            tok.setSatisfied(false);
            boolean complained = random.nextProbability(tok.getType()
                    .getComplainProbability());
            tok.setComplained(complained);
        }
        stats.logOut(tok);

        tok.setInBuilding(false);
    }

    /**
     * @param when
     */
    public void setLastArrivalTime(double when) {
        if (customersGenerated > 0)
            throw new IllegalStateException("Can't set the lastArrivalTime"
                    + " after customers created");
        if (when < 0)
            throw new IllegalArgumentException("Bad lastArrivalTime");
        lastArrivalTime = when;
    }

    public int getDMVCapacity() {
        return theDMVCapacity;
    }

    public CustomerDistribution getDistributionFor(double now) {
        CustomerDistribution ret = custDists[custDists.length - 1];
        for (int d = 0; d < custDists.length; ++d) {
            CustomerDistribution cd1 = custDists[d];
            if (now >= cd1.timeStarts && now <= cd1.timeEnds) {
                ret = cd1;
                break;
            }
        }
        return ret;
    }

    private void makeNewCIT(CustomerType ct, double now) {
        ++customersGenerated;
        double avgTime = currentDistribution.arrivalDistribution.get(ct);
        CIT cit = new CIT(ct);
        cit.setArrivalTime(now + random.nextExponential(avgTime));
        cit.setGiveupTime(cit.getArrivalTime() + ct.getWaitTime(random));
        Task task = ct.pickTask(random);
        cit.setTask(task);
        cit.setWillTakeTime(ct.getTaskTime(random, task));
        nextCITs.put(ct, cit);
    }

    private void setCurrentDistribution(CustomerDistribution cd, double now) {
        currentDistribution = cd;
        for (CustomerType ct : EnumSet.allOf(CustomerType.class)) {
            makeNewCIT(ct, now);
        }
    }

    public CIT getNextCIT(double now) {
        if (now >= lastArrivalTime)
            return null;
        CustomerDistribution cd = getDistributionFor(now);
        if (cd != currentDistribution)
            setCurrentDistribution(cd, now);

        // Find the nearest guy to go
        CIT which = null;
        double nearest = 0;
        for (CustomerType ct : nextCITs.keySet()) {
            CIT cit = nextCITs.get(ct);
            if (which == null || cit.getArrivalTime() < nearest) {
                which = cit;
                nearest = which.getArrivalTime();
            }
        }

        if (which == null)
            throw new IllegalStateException("No CIT found?");

        // Spin the next guy of that type after the one we just shipped.
        makeNewCIT(which.getType(), nearest);
        stats.logIn(which);

        return which;
    }

    public void setStaff(int nManagers, int nBadTellers, int nGoodTellers) {
        if (this.nManagers != -1)
            throw new IllegalStateException("setStaff already called");
        if (nManagers < 0 || nBadTellers < 0 || nGoodTellers < 0)
            throw new IllegalArgumentException("Negative staff level");
        this.nManagers = nManagers;
        this.nBadTellers = nBadTellers;
        this.nGoodTellers = nGoodTellers;
    }

    private void load(Reader staff, Reader world) throws InputMismatchException {
        loadStaff(staff);
        loadWorld(world);
    }

    public void loadStaff(Reader r) {
        Scanner sc = new Scanner(r);
        this.lastArrivalTime = sc.nextDouble();
        if (this.lastArrivalTime < 0)
            throw new InputMismatchException("Bad last arrival time");
        sc.nextLine();
        this.nManagers = sc.nextInt();
        this.nGoodTellers = sc.nextInt();
        this.nBadTellers = sc.nextInt();
        sc.nextLine(); // eat rest of line
    }

    private void loadWorld(Reader r) {
        Scanner sc = new Scanner(r);
        name = sc.nextLine();
        theDMVCapacity = sc.nextInt();
        sc.nextLine(); // eat rest of line
        Task.readCosts(sc);
        this.sManager = sc.nextDouble();
        this.sGoodTeller = sc.nextDouble();
        this.sBadTeller = sc.nextDouble();
        sc.nextLine(); // eat rest of line
        CustomerType.readInfo(sc);
        loadDistributions(sc);

        currentDistribution = null;
        CustomerDistribution cd = getDistributionFor(0);
        if (cd != currentDistribution)
            setCurrentDistribution(cd, 0);
    }

    public void save(Writer staff, Writer world) throws IOException {
        saveStaff(staff);
        saveWorld(world);
    }

    private void saveStaff(Writer staff) throws IOException {
        staff.write(this.lastArrivalTime + "\tLast arrival time\n");
        staff.write(nManagers + " " + nGoodTellers + " " + nBadTellers
                + "\tStaff\n");
    }

    private void saveWorld(Writer world) throws IOException {
        world.write(name + "\n");
        world.write(this.theDMVCapacity + "\tDMV capacity\n");
        Task.writeCosts(world);
        world.write(sManager + " " + sGoodTeller + " " + sBadTeller
                + "\tStaff Costs\n");
        CustomerType.saveInfo(world);
        saveDistributions(world);
    }

    private void saveDistributions(Writer w) throws IOException {
        w.write(this.custDists.length + "\tNumber of customer distributions\n");
        for (CustomerDistribution cd : custDists)
            w.write(cd + "\n");
    }

    private void loadDistributions(Scanner sc) throws InputMismatchException {
        int n = sc.nextInt();
        if (n <= 0)
            throw new InputMismatchException("Bad # of distributions");
        sc.nextLine();
        this.custDists = new CustomerDistribution[n];
        for (int i = 0; i < n; ++i) {
            String line = sc.nextLine();
            custDists[i] = CustomerDistribution.fromString(line);
        }
    }

    public static void write(Writer w, String m) {
        try {
            w.write(m);
        } catch (IOException e) {
            throw new Error(e);
        }
    }

    public boolean getScore(Writer report) {
        write(report, "SCORING REPORT FOR '" + name + "'\n");
        Statistics.CustomerReport cr = stats.new CustomerReport();
        write(report, cr.toString());

        Statistics.BudgetReport br = stats.new BudgetReport();
        write(report, "\n" + br.toString());

        Statistics.EmploymentReport er = stats.new EmploymentReport(cr, br);
        write(report, "\n" + er.toString());
        return er.isPromoted();
    }

    public World() {
        String stdStaffLoad = "36000.0\tLast arrival time\n"
                + "0 1 19\tStaff\n";

        String stdWorldLoad = "Builtin default world\n"
                + "350 DMV capacity\n"
                + "27 43 81   Task Costs\n"
                + "64134 87341 32100   Staff Costs (mgr, gtlr, btlr)\n"
                + "3600 20 100 30 200 40 300 STUDENT wait (weight time)*tasks\n"
                + "3000 20 60 30 70 40 100 BIKER wait (weight time)*tasks\n"
                + "900 1 50 1 100 1 150    WORKER wait (weight time)*tasks\n"
                + "2400 2 500 2 600 2 700    RETIREE wait (weight time)*tasks\n"
                + "10   Number of customer distributions\n"
                + "0 3600 100000 100000 120 180\n"
                + "3600 7200 100000 100000 60 120\n"
                + "7200 10800 10000 100000 10000 120\n"
                + "10800 14400 1000 5000 10000 180\n"
                + "14400 18000 500 5000 300 1000\n"
                + "18000 21600 10 10 10 30\n" + "21600 25200 200 100 1000 60\n"
                + "25200 28800 200 600 10000 120\n"
                + "28800 32400 100 300 10000 600\n"
                + "32400 36000 30 200 300 900\n";

        load(new StringReader(stdStaffLoad), new StringReader(stdWorldLoad));
    }

    public void setXRandom(XRandom r) {
        random = r;
    }

    public XRandom getXRandom() {
        return random;
    }

    public int getManagers() {
        return nManagers;
    }

    public int getGoodTellers() {
        return nGoodTellers;
    }

    public int getBadTellers() {
        return nBadTellers;
    }

    public World(Reader staff, Reader world) throws InputMismatchException {
        load(staff, world);
    }
}
